#!/usr/bin/perl # fvwm-desktop, the underdocumented Fvwm desktop! # # To use, PipeRead 'fvwm-desktop -init' at the top of your .fvwm2rc, and # PipeRead 'fvwm-desktop -start' in StartFunction. The latter is not necessary # if you use neither desktop icons nor a taskbar. # # A few gotchas and esoterica that you should be aware of: # # + The filename of a menu item becomes the identifier for that item. Several # fields can be extrapolated from this identifier if you don't specifically # provide them in the menu file. In particular, the id is also the default # icon name, and the default display name is generated by replacing # underscores with spaces and capitalizing words. # + Each application identifier must be unique against all others--not just # those in the same directory. # + A ".directory" file inside a directory can be used to set specific # key-value data for that submenu (e.g. display name). # + A menu file can have comment lines, starting with '#'. # + You can keep personal keybindings under $USER_DIR/keys. Each line has the # format =. # + The icon "start" will be displayed to the left of "Start" on the taskbar. # + The icon "add_icon" will be displayed on the taskbar button which allows # users to add taskbar launchers. I use a right arrow. # + The icons "check" and "nocheck" are used to mark selected and deselected # desktop options, respectively. # + fvwm-desktop uses colorset #1 use strict; sub my_die; # Constants my $PROG_NAME = 'fvwm-desktop'; my %CL_OPTIONS = ( init => \&init, start => \&start, toggle => \&toggle, 'add-desktop-icon' => \&add_desktop_icon, 'remove-desktop-icon' => \&remove_desktop_icon, 'add-taskbar-icon' => \&add_taskbar_icon, 'remove-taskbar-icon' => \&remove_taskbar_icon, ); my @DESKTOP_OPTIONS = ( 'enable_desktop_icons', 'enable_taskbar', 'autohide_taskbar' ); my %OPTION_DEFAULTS = ( enable_desktop_icons => 1, enable_taskbar => 1, autohide_taskbar => 0, ); my ($START_MENU, $ADD_DESKTOP_ICON_MENU, $ADD_TASKBAR_ICON_MENU) = (1,2,3); #menu types my %MENU_PREFIXES = ( $START_MENU => 'sm', $ADD_DESKTOP_ICON_MENU => 'adi', $ADD_TASKBAR_ICON_MENU => 'ati' ); # Configurable Constants my $SHARE_DIR = "/usr/X11R6/share/$PROG_NAME"; my $USER_DIR = "$ENV{HOME}/.$PROG_NAME"; my $DESKTOP_FONT = '-adobe-helvetica-medium-r-*-*-12-*-*-*-*-*-*-*'; my $ICON_EXTENSION = 'png'; my $WINDOW_LIST = 'WindowList NoGeometryWithInfo, NoDeskNum, NoNumInDeskTitle, SortClassName'; my $DESKTOP_GRID_WIDTH = 100; my $DESKTOP_GRID_HEIGHT = 70; my $DESKTOP_WIDTH = 1152; my $DESKTOP_HEIGHT = 830; my $TASKBAR_THICKNESS = 25; my $AUTOHIDE_THICKNESS = 1; # Derived Constants my $DESKTOP_MAX_ROWS = int($DESKTOP_HEIGHT / $DESKTOP_GRID_HEIGHT); my $DESKTOP_MAX_COLS = int($DESKTOP_WIDTH / $DESKTOP_GRID_WIDTH); my $MENU_DIR = "$SHARE_DIR/menu"; my $ICON_DIR = "$SHARE_DIR/icons"; # Globals my %config; my $menu; my %menu_flat; my @desktop_icons; my @taskbar_icons; ### Begin Main ### my $option = shift; $option =~ s/^-+//; my $func = $CL_OPTIONS{$option} or usage_abort(); create_user_dir(); read_all(); &$func(@ARGV); #### End Main #### # Main Functions sub init { print "ImagePath +:$ICON_DIR\n"; print "Style * NoIcon\n"; set_menu(); set_options_menu(); set_mini_icons(); set_desktop_icons(); set_taskbar(); set_default_keybindings(); get_and_set_user_keybindings(); } sub start { print "FvwmButtons FvwmDesktop\n" if @desktop_icons && $config{enable_desktop_icons}; print "FvwmTaskBar\n" if $config{enable_taskbar}; } sub add_desktop_icon { my $name = shift; $name eq '' and usage_abort(); return unless list_find($name, \@desktop_icons) < 0; push @desktop_icons, $name; write_desktop_icons(); reload_desktop(); } sub remove_desktop_icon { my $name = shift; $name eq '' and usage_abort(); my $index = list_find($name, \@desktop_icons); $index > -1 or return; remove_list_element(\@desktop_icons, $index); write_desktop_icons(); reload_desktop(); print "DestroyMenu ${name}_dt_icon_menu\n"; } sub add_taskbar_icon { my $name = shift; $name eq '' and usage_abort(); return unless list_find($name, \@taskbar_icons) < 0; push @taskbar_icons, $name; write_taskbar_icons(); reload_taskbar(); } sub remove_taskbar_icon { my $name = shift; $name eq '' and usage_abort(); my $index = list_find($name, \@taskbar_icons); $index > -1 or return; remove_list_element(\@taskbar_icons, $index); write_taskbar_icons(); reload_taskbar(); print "DestroyMenu ${name}_tb_icon_menu\n"; } sub toggle { my $key = shift; my $new_val = ($config{$key} ? 0 : 1); $config{$key} = $new_val; if ($key eq 'enable_desktop_icons') { if ($new_val) { print "FvwmButtons FvwmDesktop\n" if @desktop_icons; } else { print "KillModule FvwmButtons FvwmDesktop\n"; } } elsif ($key eq 'enable_taskbar') { $new_val ? print "FvwmTaskBar\n" : print "KillModule FvwmTaskBar\n"; } elsif ($key eq 'autohide_taskbar') { print "KillModule FvwmTaskBar\n" if $config{enable_taskbar}; $new_val ? print "*FvwmTaskBar: AutoHide $AUTOHIDE_THICKNESS\n" : print "DestroyModuleConfig FvwmTaskBar: AutoHide\n"; print "FvwmTaskBar\n" if $config{enable_taskbar}; } else { my_die "Unrecognized property for $PROG_NAME -toggle ($key)"; } write_config(); set_options_menu(); } # Helper Functions sub create_user_dir { -d $USER_DIR or mkdir $USER_DIR or my_die "Can't mkdir $USER_DIR ($!)"; } sub read_all { read_config(); read_menu(); read_desktop_icons(); read_taskbar_icons(); } sub read_config { %config = %OPTION_DEFAULTS; read_hash_file("$USER_DIR/config", \%config); } sub write_config { write_hash_file("$USER_DIR/config", \%config); } sub read_menu { chdir $MENU_DIR or ($menu = [], return); $menu = read_submenu('.'); } sub read_submenu { my $dir = shift; my @contents; opendir DIR, $dir or return []; my @files = grep(!/^\.\.?$/, readdir DIR); closedir DIR; for my $file (@files) { my $path = ($dir eq '.' ? "$file" : "$dir/$file"); my $menu_item = read_menu_item($file, $path); -d $path and $menu_item->{contents} = read_submenu($path); push @contents, $menu_item; $menu_flat{$file} = $menu_item; } @contents = sort {$a->{section} <=> $b->{section} || submenus_last($a, $b) || $a->{order} <=> $b->{order} || $a->{label} cmp $b->{label}} @contents; return \@contents; } sub submenus_last { my ($a, $b) = @_; return ($a->{submenu} && !$b->{submenu} ? 1 : $b->{submenu} && !$a->{submenu} ? -1 : 0); } sub read_menu_item { my ($file, $path) = @_; my %data = (name => $file); if (-d $path) { $data{submenu} = $path; $data{submenu} =~ tr|/|_|; $data{action} = "Popup sm_sub_$data{submenu}"; $path .= '/.directory'; } read_hash_file($path, \%data); $data{label} = make_display_name($file) unless exists $data{label}; $data{icon} = "$file.${ICON_EXTENSION}" unless exists $data{icon}; $data{large_icon} = "large/$file.${ICON_EXTENSION}" unless exists $data{large_icon}; $data{action} = "Exec $data{exec}" if exists $data{exec}; if (exists $data{window_id}) { my @window_ids = split(/\|/, $data{window_id}); $data{window_id} = \@window_ids; } return \%data; } sub read_desktop_icons { read_array_file("$USER_DIR/desktop", \@desktop_icons); } sub write_desktop_icons { write_array_file("$USER_DIR/desktop", \@desktop_icons); } sub read_taskbar_icons { read_array_file("$USER_DIR/taskbar", \@taskbar_icons); } sub write_taskbar_icons { write_array_file("$USER_DIR/taskbar", \@taskbar_icons); } sub set_options_menu { print <<"END"; DestroyMenu recreate DesktopOptionsMenu AddToMenu DesktopOptionsMenu "Desktop Options" Title END for my $option (@DESKTOP_OPTIONS) { my $label = make_display_name($option); print "+ '$label"; $config{$option} ? print "%check.png%" : print "%nocheck.png%"; print "' PipeRead '$PROG_NAME -toggle $option'\n"; } } sub set_menu { set_submenu('', 'Start Menu', '', 'StartMenu', $menu, $START_MENU); set_submenu('', 'Add A Desktop Icon', '', 'AddDesktopIconMenu', $menu, $ADD_DESKTOP_ICON_MENU); set_submenu('', 'Add A Taskbar Icon', '', 'AddTaskbarIconMenu', $menu, $ADD_TASKBAR_ICON_MENU); print <<"END"; Mouse 1 R A Menu StartMenu Mouse 2 R A $WINDOW_LIST Mouse 3 R A Menu AddDesktopIconMenu Key F15 A N Menu StartMenu END } sub set_submenu { my ($name, $label, $icon, $submenu_id, $contents, $menu_type) = @_; my $is_add_icon_menu = ($menu_type != $START_MENU); my @submenus; print "AddToMenu $submenu_id '$label' Title\n"; if ($is_add_icon_menu && $name ne '') { print "+ \"[Add This Submenu]%$icon%\" PipeRead '$PROG_NAME -add-desktop-icon $name'\n"; } my $prev_section = 0; for (@$contents) { next if $is_add_icon_menu && $_->{no_add_icon}; print "+ '' Nop\n" if $_->{section} != $prev_section; print "+ \"$_->{label}%$_->{icon}%\" "; if (exists $_->{submenu}) { my $submenu_id = "$MENU_PREFIXES{$menu_type}_sub_$_->{submenu}"; push @submenus, [$_->{name}, $_->{label}, $_->{icon}, $submenu_id, $_->{contents}, $menu_type]; print "Popup $submenu_id"; } elsif ($is_add_icon_menu) { my $opt = ($menu_type == $ADD_DESKTOP_ICON_MENU ? 'add-desktop-icon' : 'add-taskbar-icon'); print "PipeRead '$PROG_NAME -$opt $_->{name}'"; } else { print "$_->{action}"; } print "\n"; $prev_section = $_->{section}; } set_submenu(@$_) for @submenus; } sub set_mini_icons { while (my ($name, $data) = each %menu_flat) { next unless exists $data->{window_id}; print "Style \"$_\" MiniIcon $data->{icon}\n" for @{$data->{window_id}}; } } sub set_desktop_icons { print <<"END"; Colorset 1 Transparent, fg #D0D0D0 Style FvwmDesktop BorderWidth 0, CirculateSkip, NeverFocus, NoHandles, NoTitle Style FvwmDesktop ParentalRelativity, StaysOnBottom, Sticky, WindowListSkip *FvwmDesktop: Frame 0 *FvwmDesktop: Colorset 1 *FvwmDesktop: Font "$DESKTOP_FONT" *FvwmDesktop: BoxSize fixed *FvwmDesktop: ButtonGeometry ${DESKTOP_GRID_WIDTH}x${DESKTOP_GRID_HEIGHT}+0+0 END config_desktop_icons(); } sub config_desktop_icons { return unless @desktop_icons; my $num_icons = scalar @desktop_icons; my $num_cols = int($num_icons / $DESKTOP_MAX_ROWS); $num_cols++ if $num_icons % $DESKTOP_MAX_ROWS; my $num_rows = ($num_cols == 1 ? $num_icons : $DESKTOP_MAX_ROWS); print <<"END"; *FvwmDesktop: Rows $num_rows *FvwmDesktop: Columns $num_cols END my $filler_height = ($num_rows * $num_cols) - $num_icons; if ($filler_height) { my $filler_xpos = $num_cols - 1; my $filler_ypos = $num_icons % $num_rows; print <<"END"; *FvwmDesktop: (1x$filler_height+$filler_xpos+$filler_ypos \\ Action (Mouse 1) Menu StartMenu, \\ Action (Mouse 2) "$WINDOW_LIST", \\ Action (Mouse 3) Menu AddDesktopIconMenu) END } my ($row, $col) = (0, 0); for my $name (@desktop_icons) { my $data = $menu_flat{$name} or next; print <<"END"; *FvwmDesktop: (+$col+$row Title "$data->{label}", \\ Icon $data->{large_icon}, \\ Action (Mouse 1) $data->{action}, \\ Action (Mouse 3) Popup ${name}_dt_icon_menu) DestroyMenu recreate ${name}_dt_icon_menu AddToMenu ${name}_dt_icon_menu "$data->{label} Icon" Title + "Remove From Desktop" PipeRead '$PROG_NAME -remove-desktop-icon $name' END $row++; $row == $num_rows and ($row = 0, $col++); } } sub config_taskbar_icons { print "*FvwmTaskBar: Button Icon add_icon.$ICON_EXTENSION, Action Menu AddTaskbarIconMenu\n"; for my $name (@taskbar_icons) { my $data = $menu_flat{$name} or next; print <<"END"; *FvwmTaskBar: Button Icon $data->{icon}, Action (Mouse 1) $data->{action}, \\ Action (Mouse 3) Popup ${name}_tb_icon_menu DestroyMenu recreate ${name}_tb_icon_menu AddToMenu ${name}_tb_icon_menu "$data->{label} Icon" Title + "Remove From Taskbar" PipeRead '$PROG_NAME -remove-taskbar-icon $name' END } } sub set_taskbar { print <<"END"; Style FvwmTaskBar BorderWidth 3, CirculateSkip, NeverFocus, NoHandles, NoTitle Style FvwmTaskBar StaysOnTop, Sticky, WindowListSkip AddToFunc TBSelectWindow + I Iconify off + I Focus + I Raise *FvwmTaskBar: Geometry +0-0 *FvwmTaskBar: Font -*-arial-medium-r-*-*-9-*-*-*-*-*-*-* *FvwmTaskBar: SelFont -*-arial-bold-r-*-*-9-*-*-*-*-*-*-* *FvwmTaskBar: StatusFont -*-arial-medium-r-*-*-9-*-*-*-*-*-*-* *FvwmTaskBar: Fore #000000 *FvwmTaskBar: Back #B0B0B0 *FvwmTaskBar: IconBack #808080 *FvwmTaskBar: StartIcon start.$ICON_EXTENSION *FvwmTaskBar: ClockFormat %H:%M:%S %p *FvwmTaskBar: AutoStick *FvwmTaskBar: UseSkipList *FvwmTaskBar: ShowTips *FvwmTaskBar: IgnoreOldMail *FvwmTaskBar: Action Click1 TBSelectWindow *FvwmTaskBar: Action Click2 Iconify *FvwmTaskBar: Action Click3 Close END if ($config{autohide_taskbar}) { print "*FvwmTaskBar: AutoHide $AUTOHIDE_THICKNESS\n"; } config_taskbar_icons(); } sub set_default_keybindings { while (my ($name, $data) = each %menu_flat) { set_keybinding($data->{key}, $data->{action}) if exists $data->{key}; } } sub get_and_set_user_keybindings { my %keybindings; read_hash_file("$USER_DIR/keys", \%keybindings); while (my ($app, $key) = each %keybindings) { chomp; my $app_data = $menu_flat{$app} or next; set_keybinding($key, $app_data->{action}); } close F; } sub set_keybinding { my ($key, $action) = @_; my $modifiers; if ($key =~ /^(.+)-(['"]?)([^-]*.)\2$/) { $modifiers = $1; $modifiers =~ tr/-//d; $key = $3; } else { $modifiers = 'N'; } print "Key $key A $modifiers $action\n"; } sub reload_desktop { print <<"END"; KillModule FvwmButtons FvwmDesktop DestroyModuleConfig FvwmDesktop: Rows DestroyModuleConfig FvwmDesktop: Columns DestroyModuleConfig FvwmDesktop: (* END config_desktop_icons(); print "FvwmButtons FvwmDesktop\n"; } sub reload_taskbar { print <<"END"; KillModule FvwmTaskBar DestroyModuleConfig FvwmTaskBar: Button * END config_taskbar_icons(); print "FvwmTaskBar\n"; } sub read_array_file { my ($file, $array) = @_; open F, "<$file"; while () { chomp; next if /^#/ || /^\s*$/; push @$array, trim_whitespace($_); } close F; } sub write_array_file { my ($file, $array) = @_; open F, ">$file" or my_die "Can't open $file for writing ($!)"; for (@$array) { print F "$_\n" or my_die "Write error on $file ($!)"; } close F or my_die "Can't close $file after writing ($!)"; } sub read_hash_file { my ($file, $hash) = @_; open F, "<$file" or return 0; while () { chomp; next if /^#/ || /^\s*$/; my ($key, $val); if (/=/) { ($key, $val) = split(/=/, $_, 2); $key = trim_whitespace($key); $val = trim_whitespace($val); } else { $key = trim_whitespace($_); $val = 1; } $$hash{$key} = $val; } close F; return 1; } sub write_hash_file { my ($file, $hash) = @_; open F, ">$file" or my_die "Can't open $file for writing ($!)"; while (my ($key, $val) = each %$hash) { print F "$key=$val\n" or my_die "Write error on $file ($!)"; } close F or my_die "Can't close $file after writing ($!)"; } sub make_display_name { my $name = shift; $name =~ tr/_-/ /; # convert underscores to spaces $name =~ s/\b([a-z])/\U$1/g; # capitalize words return $name; } sub trim_whitespace { my $str = shift; $str =~ s/^\s+//; $str =~ s/\s+$//; return $str; } sub list_find { my ($item, $list) = @_; for (my $i=0; $i < @$list; $i++) { $$list[$i] eq $item and return $i; } return -1; } sub remove_list_element { my ($list, $index) = @_; splice(@$list, $index, 1, ()); } sub usage_abort { print STDERR <<"END"; Usage: $PROG_NAME -init (at the top of .fvwm2rc) or $PROG_NAME -start (in StartFuncion) or $PROG_NAME -toggle